ReferenceCore & Routing
@nextrush/core

Core

Application class, middleware composition, and the extension system

The core package provides the foundation of every NextRush application: the Application class, middleware composition, and the extension system.

Source & internals

$ pnpm add @nextrush/core

Usually Included

Most users install nextrush which includes this package. Install @nextrush/core directly only when using non-Node.js runtimes or building custom tooling.

What It Provides

// Application
import { Application, createApp } from '@nextrush/core';
import type {
  ApplicationOptions,
  ErrorHandler,
  ListenCallback,
  Logger,
  Routable,
} from '@nextrush/core';

// Middleware utilities
import { compose, isMiddleware, flattenMiddleware } from '@nextrush/core';
import type { ComposedMiddleware } from '@nextrush/core';

// Error classes (re-exported from @nextrush/errors)
import {
  HttpError,
  NotFoundError,
  BadRequestError,
  UnauthorizedError,
  ForbiddenError,
  InternalServerError,
  NextRushError,
} from '@nextrush/core';

// Re-exported types for convenience
import type {
  Context,
  Middleware,
  Extension,
  ExtensionContext,
  Next,
  HttpMethod,
  HttpStatusCode,
  RouteHandler,
  RouteParams,
  QueryParams,
  ContextState,
} from '@nextrush/core';

// Re-exported constants
import { HttpStatus, ContentType } from '@nextrush/core';

Application

The Application class is the container for your middleware stack and extensions.

Creating an Application

import { createApp } from '@nextrush/core';

// Default options
const app = createApp();

// With options
const app = createApp({
  env: 'production',
  proxy: true,
});

Application Options

ApplicationOptions

PropertyTypeDescription
env'development' | 'production' | 'test'= 'development'Environment mode
proxyboolean= falseTrust proxy headers (X-Forwarded-For, etc.)
logger?LoggerPluggable logger instance. Defaults to a silent no-op logger. Pass console for quick development logging.
router?RouterA router the app owns. Route methods (app.get, app.post, …) delegate to it. The `nextrush` meta-package's createApp() injects one automatically.
container?ContainerA per-app DI container, exposed to extensions and registrars (e.g. registerControllers). Injected automatically by `nextrush/class`.

Environment Mode

// Development: request errors logged server-side via app.logger
const app = createApp({ env: 'development' });

// Production: request errors not logged; responses never leak details
const app = createApp({ env: 'production' });

// Test: for automated testing
const app = createApp({ env: 'test' });

Logger

The logger option accepts any object implementing the Logger interface:

Logger

PropertyTypeDescription
error(...args: unknown[]) => voidLog error-level messages
warn(...args: unknown[]) => voidLog warning-level messages
info(...args: unknown[]) => voidLog informational messages
debug(...args: unknown[]) => voidLog debug-level messages

Any object with these methods works, including console and structured loggers like pino.

// Quick development logging
const app = createApp({ logger: console });

// Production structured logger (e.g. pino)
const app = createApp({ logger: pino() });

The logger is available on app.logger and is used internally by the error handler and extension setup/destroy hooks.

Proxy Mode

Enable when behind a reverse proxy (nginx, AWS ALB, Cloudflare):

const app = createApp({ proxy: true });

Middleware Registration

Basic Usage

// Single middleware
app.use(async (ctx) => {
  console.log(`${ctx.method} ${ctx.path}`);
  await ctx.next();
});

// Multiple middleware at once
app.use(cors(), helmet(), json());

// Method chaining
app.use(cors()).use(helmet()).use(json());

Router Composition

Mount routers directly on the application using app.route() — Hono-style composition:

import { createApp } from '@nextrush/core';
import { createRouter } from '@nextrush/router';

const app = createApp();

// Create feature routers
const users = createRouter();
users.get('/', (ctx) => ctx.json([]));
users.get('/:id', (ctx) => ctx.json({ id: ctx.params.id }));

const posts = createRouter();
posts.get('/', (ctx) => ctx.json([]));

// Mount directly — clean like Hono!
app.route('/api/users', users);
app.route('/api/posts', posts);

Benefits over Classic Pattern

Classic PatternHono-Style Composition
router.use('/users', usersRouter) then app.use(router.routes())app.route('/users', users)
Requires main routerDirect mounting
Extra .routes() callNo extra calls

Classic Pattern Still Works

const router = createRouter();
router.use('/users', usersRouter);
router.use('/posts', postsRouter);
app.route('/', router);

Two Syntax Styles

Both work identically—use whichever you prefer:

// Modern: ctx.next()
app.use(async (ctx) => {
  console.log('Before');
  await ctx.next();
  console.log('After');
});

// Koa-style: (ctx, next) parameter
app.use(async (ctx, next) => {
  console.log('Before');
  await next();
  console.log('After');
});

Execution Order

Middleware runs in registration order following the onion model — code before ctx.next() runs on the way in, code after it runs on the way out. See Middleware → the onion model for the full walkthrough and diagram.

Error Handling

Custom Error Handler

Set a custom error handler with setErrorHandler(). This replaces any previously set handler — only one is active at a time.

app.setErrorHandler((error, ctx) => {
  console.error('Request failed:', error);

  if ('status' in error && typeof error.status === 'number') {
    ctx.status = error.status;
  } else {
    ctx.status = 500;
  }

  ctx.json({
    error: error.message,
    ...(app.options.env === 'development' && { stack: error.stack }),
  });
});

There is no onError() method — setErrorHandler() is the only way to set the application error handler; the name makes the replacement semantics clear (calling it again replaces the previous handler).

Default Error Behavior

Without a custom handler, the built-in handler decides what the client sees from each error's expose flag — not from the environment. HttpError/NextRushError set expose: true for 4xx and false for 5xx; a plain Error never exposes.

Error kindClient messageStack in responseServer-side log
4xx (expose: true)The error's messageNeverdevelopment / test
5xx / plain Error"Internal Server Error"Neverdevelopment / test

The client response is identical in every environment — a 500 never leaks its message, even in development. The only environment-dependent behavior is the server-side log: the default handler logs via app.logger in development and test, and stays silent in production. To include a stack trace or the raw message in responses, set a custom handler (above). See Application → error handling for the rationale.

Extension System

Extensions are the rare (~0.1%) long-lived, app-scoped services — an event bus, a database pool — that need a boot phase and a teardown phase. Most capability is middleware (app.use()), not an Extension. See Extending NextRush for the full taxonomy (Middleware, Registrar, Extension) and when to reach for each.

Registering Extensions

app.extend() is synchronous and only queues the extension — setup() runs later, at app.ready(), in registration order:

import { events } from '@nextrush/events';

app.extend(events());
await app.ready(); // adapters call this automatically before start()

app.events.emit('server:started', {});

Extension Lifecycle

// app.ready() boots every registered extension once, in order
await app.ready();

// After ready(), configuration is frozen
app.use(mw); // ❌ throws — use()/route()/extend() all throw after ready()

app.close() tears extensions down in reverse registration order, using Promise.allSettled so one failing destroy() never strands the others.

Checking for a Decoration

There is no public app.decorate() — only ctx.decorate() inside an Extension's setup(). The only public, read-only check from outside an extension is:

if (app.hasDecorator('events')) {
  console.log('events extension is active');
}

Middleware Composition

compose()

Combine multiple middleware into a single function:

import { compose } from '@nextrush/core';

const security = compose([cors(), helmet(), rateLimit({ max: 100 })]);

// Use as single middleware
app.use(security);

Utility Functions

import { isMiddleware, flattenMiddleware } from '@nextrush/core';

// Type guard
if (isMiddleware(fn)) {
  app.use(fn);
}

// Flatten nested arrays
const flat = flattenMiddleware([mw1, [mw2, mw3], [[mw4]]]);
// Result: [mw1, mw2, mw3, mw4]

Application Lifecycle

Request Handler

Adapters use app.callback() to get the request handler:

const handler = app.callback();

// Handler signature: (ctx: Context) => Promise<void>

Starting

Adapters call app.ready() (booting extensions) then app.start() when the server begins:

await app.ready();
app.start();
console.log(app.isRunning); // true

Graceful Shutdown

app.close() returns Promise<Error[]> — an array of errors from extensions that failed to destroy. An empty array means all extensions shut down cleanly.

const errors = await app.close();

// What happens:
// 1. Sets isRunning = false
// 2. Calls destroy() on extensions (reverse registration order)
// 3. Collects errors via Promise.allSettled (all extensions get a chance to destroy)
// 4. Clears decorations and the extension registry
// 5. Returns any extension destroy errors

Example shutdown hook:

process.on('SIGTERM', async () => {
  console.log('Shutting down...');
  const errors = await app.close();
  if (errors.length > 0) {
    console.error('Extension shutdown errors:', errors);
  }
  process.exit(errors.length > 0 ? 1 : 0);
});

Application Properties

app.isProduction; // boolean — true if env === 'production'
app.isRunning; // boolean — true after start() called
app.middlewareCount; // number — count of registered middleware
app.options; // ApplicationOptions — readonly config
app.logger; // Logger — configured logger instance (readonly)

Complete Example

import { createApp } from '@nextrush/core';
import { createRouter } from '@nextrush/router';
import { serve } from '@nextrush/adapter-node';

const app = createApp({
  env: 'production',
  proxy: true,
  logger: console,
});

// Error handler
app.setErrorHandler((error, ctx) => {
  app.logger.error(error);

  if ('status' in error && typeof error.status === 'number') {
    ctx.status = error.status;
  } else {
    ctx.status = 500;
  }

  ctx.json({ error: app.isProduction ? 'Internal Server Error' : error.message });
});

// Middleware
app.use(async (ctx) => {
  const start = Date.now();
  await ctx.next();
  app.logger.info(`${ctx.method} ${ctx.path} - ${Date.now() - start}ms`);
});

// Feature routers
const health = createRouter();
health.get('/', (ctx) => ctx.json({ status: 'healthy' }));

const users = createRouter();
users.get('/', (ctx) => ctx.json([]));
users.get('/:id', (ctx) => ctx.json({ id: ctx.params.id }));

// Mount routers — Hono-style composition
app.route('/health', health);
app.route('/users', users);

// Start server
serve(app, { port: 8080 });

TypeScript Types

import { createApp, Application } from '@nextrush/core';
import type {
  ApplicationOptions,
  ErrorHandler,
  ListenCallback,
  Logger,
  Routable,
  ComposedMiddleware,
} from '@nextrush/core';

// Type the application
const app: Application = createApp();

// Type the error handler
const errorHandler: ErrorHandler = (error, ctx) => {
  ctx.status = 500;
  ctx.json({ error: error.message });
};

app.setErrorHandler(errorHandler);

API Reference

Application Class

Property/MethodTypeDescription
app.use(...mw)thisRegister one or more middleware functions (chainable)
app.route(path, router)thisMount a Routable at a path prefix (Hono-style)
app.setErrorHandler(handler)thisSet the application error handler (replaces any previous handler)
app.extend(extension)thisQueue an extension. setup() runs later, at ready().
app.ready()Promise<this>Boot every registered extension's setup() once, in order. Idempotent. Freezes configuration.
app.hasDecorator(name)booleanCheck whether a name has been decorated onto the app by an extension
app.callback()(ctx: Context) => Promise<void>Build the request handler. Snapshots the middleware stack at call time.
app.start()voidMark app as running. Call after ready()use(), route(), extend() already throw post-ready().
app.close()Promise<Error[]>Graceful shutdown. Destroys extensions in reverse order. Returns errors from failed extension destroys.
app.isProductionbooleantrue when env === 'production'
app.isRunningbooleantrue after start(), false after close()
app.isReadybooleantrue after ready() has booted all extensions
app.middlewareCountnumberCount of registered middleware
app.extensionCountnumberCount of registered extensions
app.optionsApplicationOptionsReadonly configuration
app.loggerLoggerReadonly logger instance
app.routerRouter | undefinedThe app-owned router, if one was configured
app.containerContainer | undefinedThe app-owned DI container, if one was configured

compose()

function compose(middleware: Middleware[]): ComposedMiddleware;

Composes multiple middleware into a single function using the onion model.

isMiddleware()

function isMiddleware(fn: unknown): fn is Middleware;

Type guard to check if a value is a valid middleware function.

flattenMiddleware()

function flattenMiddleware(arr: (Middleware | Middleware[])[]): Middleware[];

Flattens nested middleware arrays into a single array.

See Also

Was this helpful?

On this page