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:
- Composes middleware — registers and chains middleware in insertion order
- Manages plugins — typed plugin system with lifecycle hooks and shutdown cleanup
- Centralizes error handling — a configurable error handler wraps the entire pipeline
- Produces a callback — generates the request handler function that adapters need
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:
- Plugin hooks —
extendContext()andonRequest()run for each plugin with hooks - Middleware chain — composed middleware executes in registration order (onion model)
- Response hooks —
onResponse()runs for each plugin (errors isolated per-plugin) - Error path — if anything throws, plugin
onError()hooks run first, then the app error handler
Request → extendContext → onRequest → middleware₁ → middleware₂ → … → Response
↓
onResponse hooksMinimal 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
isRunningtofalse, re-enabling configuration methods - Calls
destroy()on each plugin in reverse installation order usingPromise.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
| Property | Type | Description |
|---|---|---|
env | 'development' | 'production' | 'test'= 'development' | Environment mode |
proxy | boolean= false | Trust 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
| Property | Type | Description |
|---|---|---|
options | ApplicationOptions | Readonly configuration object (env, proxy) |
logger | Logger | Configured logger instance |
isProduction | boolean | True when env is "production" |
isRunning | boolean | True after start(), false after close() |
middlewareCount | number | Count 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 | undefinedInstalling 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:
| Environment | Response Message | Logging |
|---|---|---|
development | Full error message | logger.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; // trueCall app.close() for graceful shutdown. It returns an array of errors from plugins that failed to destroy (empty on success).
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: trueonly behind a trusted reverse proxy. When disabled (default),ctx.ipuses 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