NextRush
Concepts

Application

The entry point and orchestrator of every NextRush application

The Application class is the central orchestrator of NextRush. It composes middleware, plugins, and error handling into a request handler that adapters use to serve HTTP traffic.

The Problem

Building a web server requires coordinating multiple concerns:

  • Registering middleware in the correct order
  • Managing plugin lifecycles and shutdown cleanup
  • Handling errors consistently across all routes
  • Producing a request handler that works across runtimes (Node.js, Bun, Deno)

Without a central orchestrator, you wire these together manually for every project — and get it wrong in different ways each time.

How NextRush Solves This

The Application class provides a single entry point that:

  1. Composes middleware — registers and chains middleware in insertion order
  2. Manages plugins — typed plugin system with lifecycle hooks and shutdown cleanup
  3. Centralizes error handling — a configurable error handler wraps the entire pipeline
  4. Produces a callback — generates the request handler function that adapters need
Loading diagram...

Mental Model

Think of Application as an assembly line:

  • Middleware are the stations — each processes the request in order
  • Plugins add new stations or capabilities
  • Error handler catches defects at any station
  • callback() starts the line — adapters feed requests into it

The Application never processes requests directly. It builds the pipeline, then callback() snapshots it into a handler function.

Execution Flow

When a request arrives, the handler produced by callback() runs this sequence:

  1. Plugin hooksextendContext() and onRequest() run for each plugin with hooks
  2. Middleware chain — composed middleware executes in registration order (onion model)
  3. Response hooksonResponse() runs for each plugin (errors isolated per-plugin)
  4. Error path — if anything throws, plugin onError() hooks run first, then the app error handler
Request → extendContext → onRequest → middleware₁ → middleware₂ → … → Response

                                                                  onResponse hooks

Minimal Usage

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

const app = createApp();

const router = createRouter();
router.get('/', (ctx) => ctx.json({ status: 'ok' }));

app.route('/', router);

The createApp() factory is the recommended way to create an Application. You can also use new Application() directly.

What Happens Automatically

When you call app.callback(), the Application:

  • Snapshots the middleware stack — middleware added after callback() won't affect the returned handler
  • Collects plugin hooks — validates that hook properties (onRequest, onResponse, onError, extendContext) are callable functions
  • Wraps everything in error handling — uncaught errors flow to the configured error handler

When you call app.close():

  • Sets isRunning to false, re-enabling configuration methods
  • Calls destroy() on each plugin in reverse installation order using Promise.allSettled
  • Clears the plugin registry
  • Returns Error[] — any errors from plugins that failed to destroy

Configuration

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

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

ApplicationOptions

PropertyTypeDescription
env'development' | 'production' | 'test'= 'development'Environment mode
proxyboolean= falseTrust proxy headers (X-Forwarded-For, X-Forwarded-Proto)
logger?Logger= No-op (silent)Pluggable logger instance with error, warn, info, and debug methods

The logger accepts any object with error, warn, info, and debug methods. Pass console for quick development logging, or a structured logger like pino for production.

Application Properties

These read-only properties are available on every Application instance.

Application Properties

PropertyTypeDescription
optionsApplicationOptionsReadonly configuration object (env, proxy)
loggerLoggerConfigured logger instance
isProductionbooleanTrue when env is "production"
isRunningbooleanTrue after start(), false after close()
middlewareCountnumberCount of registered middleware functions

Middleware Registration

// 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());

Middleware executes in registration order (onion model). Each middleware runs its "before" logic, calls next(), then runs its "after" logic:

Request → cors → helmet → json → router → Response
              ↓      ↓      ↓      ↓
          (before) (before) (before) (handler)
              ↑      ↑      ↑      ↑
          (after)  (after) (after) (done)

Passing a non-function to use() throws a TypeError.

Router Mounting

Mount routers at path prefixes using app.route():

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

const app = createApp();

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([]));

app.route('/users', users);
app.route('/posts', posts);

The route() method calls router.routes() internally — you don't need to call it yourself. Prefix boundaries are enforced, so /users won't match /usersxyz.

Plugin System

Plugins extend the application through a typed interface. The plugin() method handles both synchronous and asynchronous plugins automatically.

// Synchronous plugin
app.plugin(loggerPlugin({ level: 'info' }));

// Async plugin — await the result
await app.plugin(databasePlugin({ uri: '...' }));

// Check if installed
app.hasPlugin('logger'); // boolean

// Get plugin instance
app.getPlugin<LoggerPlugin>('logger'); // T | undefined

Installing a plugin with a duplicate name throws an error. Plugins cannot be installed after app.start().

See Plugins for the full Plugin interface and lifecycle hooks.

Error Handling

Setting an Error Handler

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: app.isProduction ? 'Internal Server Error' : error.message,
  });
});

Only one error handler is active at a time. Calling setErrorHandler() replaces any previous handler. The method returns this for chaining.

Default Error Behavior

Without a custom handler, the Application uses its built-in error handler:

EnvironmentResponse MessageLogging
developmentFull error messagelogger.error() is called
production"Internal Server Error"Silent (no logging)

If the error has a numeric status property in the 400–599 range, that status code is used. Otherwise, the response gets status 500.

Logging depends on the configured logger. The default no-op logger produces no output. Pass console or a structured logger to see error logs.

HTTP Error Classes

import { NotFoundError, BadRequestError, UnauthorizedError } from '@nextrush/errors';

app.use(async (ctx) => {
  throw new NotFoundError('User not found'); // 404
  throw new BadRequestError('Invalid email'); // 400
  throw new UnauthorizedError('Token expired'); // 401
});

Lifecycle

Adapters call app.start() when the server begins listening. After this, use(), route(), and plugin() throw to prevent unsafe mutations while requests are in flight.

Configuration Locks After Start

Calling app.start() freezes the application. Any attempt to register middleware, mount routers, or install plugins after this point throws an error.

app.start();
app.isRunning; // true

Call app.close() for graceful shutdown. It returns an array of errors from plugins that failed to destroy (empty on success).

Graceful Shutdown
process.on('SIGTERM', async () => {
  const errors = await app.close();
  if (errors.length) console.error('Shutdown errors:', errors);
  process.exit(0);
});

Performance Notes

  • Middleware composition happens once at callback() time — no per-request overhead for building the chain
  • Plugin hooks are collected once during callback(), not re-scanned per request
  • Route mounting uses string prefix matching with boundary checks, not regex
  • The middleware stack is a flat array with no tree traversal per request

Security Considerations

  • Enable proxy: true only behind a trusted reverse proxy. When disabled (default), ctx.ip uses the direct connection address.
  • The default error handler hides error messages in production mode. Custom error handlers should avoid leaking stack traces or internal paths.
  • Configuration is frozen after start() — no middleware can be added while requests are in flight.

Common Mistakes

Forgetting to Mount a Router

// ❌ Router defined but never mounted
const router = createRouter();
router.get('/users', handler);

// ✅ Mount it
app.route('/api', router);

Wrong Middleware Order

// ❌ Auth middleware after routes — routes run unprotected
app.route('/api', router);
app.use(authMiddleware);

// ✅ Security middleware before routes
app.use(authMiddleware);
app.route('/api', router);

Not Awaiting Async Plugins

// ❌ Async plugin not awaited — app starts before plugin is ready
app.plugin(databasePlugin());
app.start();

// ✅ Await async plugins
await app.plugin(databasePlugin());
app.start();

Not Awaiting Close

// ❌ Plugins may not clean up
app.close();
process.exit(0);

// ✅ Wait for cleanup
await app.close();
process.exit(0);

When Not To Use

The Application class is the right choice for most NextRush apps. Consider alternatives when:

  • You need a standalone router without middleware or plugins — use createRouter() directly
  • You're building a lightweight function handler for serverless — the adapter alone may suffice

See Also

On this page